SwiftUI 作為一個聲明式的 UI 框架,幫我們處理了幾乎所有關於 介面UI 和 數據 之間的交互,通過數據流向對視圖更新等操作,應用程序運行時,關於介面的修改,只能透過修改數據來間接完成,而不是直接對介面面進行修改操作,SwiftUI 又以單一數據源(single source of truth)為核心,構建了數據驅動狀態更新的機制
而為了實現數據和UI 的綁定,需要透過不同的關鍵字屬性包裝器Property Wrapper 來向SwiftUI 描述它們之間的關係,用來進行狀態管理
Property
一般在View Strcut 裡定義的var
、let
屬性,只能用來讀取
@State 可變屬性(可觀察屬性)
Struct 結構裡的屬性不能直接修改,需把**@State關鍵字放置到屬性前修飾,該屬性實際上會被放到Struct 的外部儲存**起來,這意味著SwiftUI 能夠隨時銷毀和重建Struct 而不會丟失屬性的值
當被@State
包裝的屬性改變時,SwiftUI 會重新渲染使用到該屬性的視圖,系統會自動的重新計算View 的body 部分,構建出View Tree,由於View 都是Struct,SwiftUI 每次構建這個View Tree 都極快,這使得性能有很強的保障
視圖經常會被系統重建,所以需要給屬性賦一個預設值,若屬性被標記為@State
,系統之後會使用儲存的變量的值,而不是每次都使用初始化的預設值
被@State
包裝的屬性一定要用private
修飾,並且這個變量只能在當前View 的body 內修改,所以它的使用場景是只影響當前View 內部的變化的操作
當一個屬性使用@State
關鍵詞時,Swift 會為這個屬性添加一些額外的方法,比如Boolean 類型會有toggle()
方法,用於在true/false 之間切換
範例:
struct ContentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text("Switch State: \(isOn ? "On" : "Off")")
Button("change") {
isOn.toggle()
}
.padding()
}
}
}
@ObservedObject
一般情況下數據來自本地Local 端或者遠端Remote API,這些數據預設與SwiftUI 沒有任何關係,而我們需要建立數據與介面的依賴關係就要使用到@ObservedObject
和@Published
,@ObservedObject
允許外部進行使用和修改
@ObservedObject
告訴SwiftUI,這個物件是可以被觀察的,裡面含有被@Published
包裝了的屬性,而@ObservedObject
包裝的物件必須是Class的物件,且必須實作ObservableObject
,不能是Struct
@ObservedObject
搭配著@Published
一起使用,允許我們創建出能夠被自動觀察的物件屬性,SwiftUI 會自動監視觀察這個屬性,一旦發生改變,會自動修改與該屬性綁定的介面
範例:
建立一個ObservedObject 的類別
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
當被@Published
包裝的變量改變時,body 會使用新值重新加載刷新頁面
struct ContentView: View {
@ObservedObject var person = Contact(name: "Ryder", age: 27)
var body: some View {
VStack {
Text("name:\(person.name)")
Button("change") {
person.name = "Ryder2"
}
}
}
}
@StateObject
與@ObservedObject
功能類似,區別在於當view 刷新時被@ObservedObject
包裝的屬性會重置到初始值,而被StateObject使用的不會,就是說隨著View 的創建被多次創建(復用)@ObservedObject
包裝的屬性將會多次創建(浪費資源),而@StateObject
包裝的屬性只會被創建一次(節約資源),@StateObject
基本上來說就是一個針對Class 的@State
升級版,因此除非在某些必要的情況下需要使用ObservedObject 之外,大多數情況都適用於StateObject
@Binding 屬性綁定
聲明一個屬性是從外部獲取的,並且與外部是共享的,作用是在保存狀態的屬性和更改數據的視圖之間創建雙向連接,將當前屬性連接到儲存在別處的單一數據源(single source of truth),而不是直接儲存數據。將儲存在別處的值的屬性轉換為引用類型,在使用時需要在變量名前加$符號
通常使用場景是把當前View 中的@State
值傳遞給其子View,要在子View 裡修改上層數據,需透過修改@Binding
屬性觸發父視圖@State
改變並重新計算body 後更新視圖,可以實現反向數據流的功能(雙向綁定),如果在子View 使用的是@State
值,子視圖中的@State
屬性會成為新的單一數據源,那麼子View 中對值的某個屬性進行修改,父View 就不會得到變化,所以需要在子視圖中使用@Binding
範例:
struct ContentView: View {
@State private var isWifiOn = false
var body: some View {
VStack {
Text("Wifi state: \(isWifiOn ? "On" : "Off")")
// 向下傳遞子視圖的binding參數 isOn,透過$符號傳遞引用 isWifiOn
WifiView(isOn: $isWifiOn)
.padding()
}
}
}
struct WifiView:View {
// 引用外部傳遞的參數
@Binding var isOn:Bool
var body: some View{
VStack{
Button("wifi change") {
// 切換時,由於Binding 機制的作用,會修改引用的外部單一數據源
isOn.toggle()
}
}
}
}
@EnvironmentObject
主要是為了在整個應用程序中共享物件,在View 物件裡通過environmentObject()
方法向下傳遞全局數據,底下所有的視圖皆共享同一個環境物件並且可以監聽變更
範例:
// 設定要共享的數據類別
class DataSource: ObservableObject {
@Published var counter = 0
}
struct ContentView: View {
let dataSource = DataSource()
var body: some View {
VStack {
Button("Click") {
// 改變共享的數據,其他有用此數據的視圖也將重載
dataSource.counter += 1
}
DisplayView()
}
// 通過在View 使用`environmentObject()`方法向下傳遞全局數據
.environmentObject(dataSource)
}
}
struct DisplayView: View {
@EnvironmentObject var dataSource: DataSource
var body: some View {
Text("Click times:\(dataSource.counter)")
}
}
@Environment 系統環境數據
與@EnvironmentObject
作用不同,@Environment
是從不同硬體設置及軟體環境中取出預設定義的值(系統內建屬性),比如獲得當前是深色模式還是正常模式,螢幕的大小等等
範例:
@Environment(\.colorScheme) var colorScheme: ColorScheme
if colorScheme == .light {
//正常模式畫面
} else {
//深色模式畫面
}